/**
 * File: IdentifierFilepositionIndexImpl.java
 * Created by: mhaimel
 * Created on: 15 Jul 2009
 * CVS:  $Id: IdentifierFilepositionIndexImpl.java,v 1.3 2009/12/07 10:05:42 mhaimel Exp $
 */
package uk.ac.ebi.curtain.service.impl;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import uk.ac.ebi.curtain.service.IdentifierFilepositionIndex;
import uk.ac.ebi.curtain.util.FileIndexReader;
import uk.ac.ebi.curtain.util.FileIndexWriter;
import uk.ac.ebi.curtain.util.FileIndexUtil.FileIndexEvent;
import uk.ac.ebi.curtain.util.impl.FileIndexReaderImpl;
import uk.ac.ebi.curtain.util.impl.FileIndexWriterImpl;
import uk.ac.ebi.curtain.utils.CurtainUncheckedException;
import uk.ac.ebi.curtain.utils.concurrent.UtilsCollect;
import uk.ac.ebi.curtain.utils.file.FileInfo;
import uk.ac.ebi.curtain.utils.io.impl.FileIO;
import uk.ac.ebi.velvet.model.Strand;

import com.google.inject.Singleton;

/**
 * User has to be aware, when to open / close Reader or Writer - only one mode possible
 * @author mhaimel
 *
 */
@Singleton
public class IdentifierFilepositionIndexImpl implements IdentifierFilepositionIndex {
	private Log log = LogFactory.getLog(this.getClass());
	
	private Object LOCK_OBJ = new Object();
	
	private final File indexFile;
	private AtomicBoolean isWritemode = new AtomicBoolean(true);
	private FileIndexWriter writer = null;
	private AtomicBoolean writerClosed = new AtomicBoolean(true);
	private Map<FileIndexReader, FileIndexReader> readerMap = UtilsCollect.newConcurrentMap();
	private Map<FileIndexWriter, FileIndexWriter> writerMap = UtilsCollect.newConcurrentMap();

	private Thread shutdownThread = null;
	
	
	public IdentifierFilepositionIndexImpl( File indexFile) {
		this.indexFile = indexFile;
		registerShutdownhook();
	}
	
	private void registerShutdownhook() {
		Runtime.getRuntime().addShutdownHook(getShutdownThread());		
	}

	private Thread getShutdownThread() {
		if (null == shutdownThread){
			shutdownThread  = new Thread() {
			    public void run() { 
			    	try {
						close();
					} catch (IOException e) {
						e.printStackTrace();
					}
			    }
			};
		}
		return shutdownThread;
	}

	private File getIndexFile() {
		return indexFile;
	}

	private FileIndexWriter _origWriter(){
		if(null == writer){
			writer = new FileIndexWriterImpl(getIndexFile());	
		}
		return writer;
	}
	
	private DelegateWriter _getNewWriter(){
		return new DelegateWriter(_origWriter());
	}
	
	private FileIndexReader _origReader(){
		FileIndexReaderImpl impl = null; 
		try{
			File file = getIndexFile();
			impl = new FileIndexReaderImpl(file);
			impl.open();
		}catch (IOException e) {
			FileIO.closeQuietly(impl);
			throw new CurtainUncheckedException("Problems opening Reader for file " + getIndexFile() + "!!!",e); 
		}
		return impl;
	}
	
	private DelegateReader _getNewReader(){
		return new DelegateReader(_origReader());
	}

	public FileIndexReader getReader() {
		synchronized (LOCK_OBJ) {	
			_ensureReadable();
			DelegateReader dRead = _getNewReader();
			readerMap.put(dRead, dRead._myReader);
			return dRead;
		}
	}

	public FileIndexWriter getWriter() {
		synchronized (LOCK_OBJ) {	
			_ensureWriteable();
			DelegateWriter dWr = _getNewWriter();
			writerMap.put(dWr, dWr._myWriter);
			return dWr;
		}
	}

	private void _ensureWriteable(){
		if(!readerMap.isEmpty()){
			throw new CurtainUncheckedException("Not able to write - readers registered and not closed: " + readerMap.size());
		}
		if(!isWritemode.get() || writerClosed.get()){
			throw new CurtainUncheckedException("Change Read mode to Write mode!!");
		}
	}

	private void _ensureReadable() {
		if(!writerMap.isEmpty()){
			throw new CurtainUncheckedException("Not able to write - readers registered and not closed: " + readerMap.size());
		}

		if(isWritemode.get()){
			throw new CurtainUncheckedException("Change Write mode to Read mode!!");
		}
	}

	public boolean isReadable() {
		synchronized (LOCK_OBJ) {
			return writerMap.isEmpty() && !isWritemode.get();
		}
	}



	public boolean isWriteable() {
		synchronized (LOCK_OBJ) {	
			return readerMap.isEmpty() && isWritemode.get() && !writerClosed.get();
		}
	}

	private void _closeWriters(){
		List<FileIndexWriter> wList = new ArrayList<FileIndexWriter>(writerMap.keySet());
		for(FileIndexWriter w : wList){
			FileIO.closeQuietly(w);
		}
		if(!writerMap.isEmpty()){
			throw new CurtainUncheckedException("Write Map just cleared, but still "+writerMap.size()+" entries left -> Probem with parallel access!!!");
		}
		FileIO.closeQuietly(writer);
		writerClosed.set(true);
	}

	private void _closeReaders(){
		List<FileIndexReader> rList = new ArrayList<FileIndexReader>(readerMap.keySet());
		for(FileIndexReader r : rList){
			FileIO.closeQuietly(r);
		}
		if(!readerMap.isEmpty()){
			throw new CurtainUncheckedException("Read Map just cleared, but still "+readerMap.size()+" entries left -> Probem with parallel access!!!");
		}	
	}
	
	public boolean setReadOnly() {
		synchronized (LOCK_OBJ) {	
			_setReadMode();
			return true;
		}
	}
	
	private void _setReadMode(){		
		_closeWriters();
		isWritemode.set(false);
		
	}

	private void _setWriteMode(){
		_closeReaders();
		if(writerClosed.get()){
//			try{
//				_origWriter().openWrite(true);
//			}catch (IOException e) {
//				FileIO.closeQuietly(writer);
//				throw new CurtainUncheckedException("Problems opening Writer for file " + getIndexFile() + "!!!",e); 
//			}
			writerClosed.set(false);
		}
		isWritemode.set(true);
	}


	public boolean setWriteOnly() {
		synchronized (LOCK_OBJ) {		
			_setWriteMode();
			return true;
		}
	}
	

	public void synchronize() {
		synchronized (LOCK_OBJ) {	
			if(getIndexFile().exists()){
				FileIndexReader reader = null; 
				FileIndexWriter writer = null; 
				try{
					_setReadMode();
					writer = _origWriter();
					reader = _origReader();
					writer.synchronize(reader);
				} finally{
					FileIO.closeQuietly(writer);
					FileIO.closeQuietly(reader);
					FileIO.closeQuietly(this);
				}				
			}
		}
	}

	public void close() throws IOException {
		synchronized (LOCK_OBJ) {	
			_closeWriters();
			_closeReaders();
		}		
	}
	
	private class DelegateWriter implements FileIndexWriter{
		

		private final FileIndexWriter _myWriter;
		private final AtomicBoolean isOpen = new AtomicBoolean(true);
		
		public DelegateWriter(FileIndexWriter _myWriter) {
			this._myWriter = _myWriter;
		}

		public void addContig(FileInfo fi, String key, Long pos) {
			_ensureOpen();
			_myWriter.addContig(fi, key, pos);
		}

		public void addContigPosition(FileInfo fi, Long pos) {
			_ensureOpen();
			_myWriter.addContigPosition(fi, pos);
		}

		public void addFile(FileInfo fi, Integer category) {
			_ensureOpen();
			_myWriter.addFile(fi, category);
		}

		public void addInsertLength(Integer category, Integer insLen) {
			_ensureOpen();
			_myWriter.addInsertLength(category, insLen);
		}

		public void addInsertLengthSd(Integer category, Integer sd) {
			_ensureOpen();
			_myWriter.addInsertLengthSd(category, sd);
		}

		public void addRead(FileInfo fi, String key, Strand strand, Long pos) {
			_ensureOpen();
			_myWriter.addRead(fi, key, strand, pos);
		}

		public void addRead(Integer fileId, String key, Strand strand,
				Long position) {
			_ensureOpen();
			_myWriter.addRead(fileId, key, strand, position);
		}

		public void addReadBatch(Integer[] fileIds, String[] keys,
				Strand[] strands, Long[] positions) {
			_ensureOpen();
			_myWriter.addReadBatch(fileIds, keys, strands, positions);
		}

		public void addReadPosition(FileInfo fi, Strand strand, Long pos) {
			_ensureOpen();
			_myWriter.addReadPosition(fi, strand, pos);
		}

		public void close() throws IOException {
			// Don't close real writer here!!!
			this.isOpen.set(false);
			writerMap.remove(this);
		}

		public Integer registerFile(FileInfo fileInfo, Integer category) {
			_ensureOpen();
			return _myWriter.registerFile(fileInfo, category);
		}
		
		private void _ensureOpen(){
			if(!isOpen.get()){
				throw new CurtainUncheckedException("Writer closed - need to get a new Writer!!!");
			}
		}

		public void openWrite(boolean append) throws IOException {
			// do nothing - should already be open
		}

		@Override
		public void synchronize(FileIndexReader reader) {
			_myWriter.synchronize(reader);
		}

		@Override
		public void addContig(Integer fileId, String key, Long pos) {
			_ensureOpen();
			_myWriter.addContig(fileId, key, pos);
		}

		@Override
		public void addFileId(Integer fid, FileInfo fi) {
			_ensureOpen();
			_myWriter.addFileId(fid, fi);
		}

		@Override
		public void addReadPosition(Integer fileId, Strand strand, Long pos) {
			_ensureOpen();
			_myWriter.addReadPosition(fileId, strand, pos);
		}

		@Override
		public void addContigPosition(Integer fileId, Long pos) {
			_ensureOpen();
			_myWriter.addContigPosition(fileId, pos);		}

		@Override
		public void addCategory(Integer category, String id) {
			_ensureOpen();
			_myWriter.addCategory(category, id);
		}
	}
	
	private class DelegateReader implements FileIndexReader{
		
		private final FileIndexReader _myReader;
		private final AtomicBoolean isOpen = new AtomicBoolean(true);

		private DelegateReader(FileIndexReader _myReader){
			this._myReader = _myReader;
			this.isOpen.set(true);
		}

		public void close() throws IOException {
			_myReader.close();
			this.isOpen.set(false);
			readerMap.remove(this);
		}

		public void open() throws IOException {
			_ensureOpen();
			_myReader.open();
		}

		public void readAll() throws IOException {
			_ensureOpen();
			_myReader.readAll();
		}

		public boolean readBlock(Integer lines) throws IOException {
			_ensureOpen();
			return _myReader.readBlock(lines);
		}

		public void setEventHandler(FileIndexEvent handler) {
			_ensureOpen();
			_myReader.setEventHandler(handler);
		}

		public void setEventHandler(FileIndexEvent handler, IndexFilter filter) {
			_ensureOpen();
			_myReader.setEventHandler(handler, filter);
		}
		
		private void _ensureOpen(){
			if(!isOpen.get()){
				throw new CurtainUncheckedException("Reader closed - need to get a new Reader!!!");
			}
		}

		public void registerFileId(Integer fileId, FileInfo info) {
			_myReader.registerFileId(fileId, info);
		}
		
	}

	public File getFile() {
		return getIndexFile();
	}	
}
